/*-------------------------------------------------------------------------
 *
 * relnode.c
 *	  关系节点查找/构造例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/optimizer/util/relnode.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <limits.h>

#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/placeholder.h"
#include "optimizer/plancat.h"
#include "optimizer/restrictinfo.h"
#include "optimizer/tlist.h"
#include "utils/hsearch.h"
#include "utils/lsyscache.h"


typedef struct JoinHashEntry
{
	Relids		join_relids;	/* 哈希键 --- 必须是第一个 */
	RelOptInfo *join_rel;
} JoinHashEntry;

static void fc_build_joinrel_tlist(PlannerInfo *fc_root, RelOptInfo *fc_joinrel,
								RelOptInfo *fc_input_rel);
static List *fc_build_joinrel_restrictlist(PlannerInfo *fc_root,
										RelOptInfo *fc_joinrel,
										RelOptInfo *fc_outer_rel,
										RelOptInfo *fc_inner_rel);
static void fc_build_joinrel_joinlist(RelOptInfo *fc_joinrel,
								   RelOptInfo *fc_outer_rel,
								   RelOptInfo *fc_inner_rel);
static List *fc_subbuild_joinrel_restrictlist(RelOptInfo *fc_joinrel,
										   List *fc_joininfo_list,
										   List *fc_new_restrictlist);
static List *fc_subbuild_joinrel_joinlist(RelOptInfo *fc_joinrel,
									   List *fc_joininfo_list,
									   List *fc_new_joininfo);
static void set_foreign_rel_properties(RelOptInfo *fc_joinrel,
									   RelOptInfo *fc_outer_rel, RelOptInfo *fc_inner_rel);
static void add_join_rel(PlannerInfo *fc_root, RelOptInfo *fc_joinrel);
static void fc_build_joinrel_partition_info(RelOptInfo *fc_joinrel,
										 RelOptInfo *fc_outer_rel, RelOptInfo *fc_inner_rel,
										 List *fc_restrictlist, JoinType fc_jointype);
static bool fc_have_partkey_equi_join(RelOptInfo *fc_joinrel,
								   RelOptInfo *fc_rel1, RelOptInfo *fc_rel2,
								   JoinType fc_jointype, List *fc_restrictlist);
static int	fc_match_expr_to_partition_keys(Expr *fc_expr, RelOptInfo *fc_rel,
										 bool fc_strict_op);
static void set_joinrel_partition_key_exprs(RelOptInfo *fc_joinrel,
											RelOptInfo *fc_outer_rel, RelOptInfo *fc_inner_rel,
											JoinType fc_jointype);
static void fc_build_child_join_reltarget(PlannerInfo *fc_root,
									   RelOptInfo *fc_parentrel,
									   RelOptInfo *fc_childrel,
									   int fc_nappinfos,
									   AppendRelInfo **fc_appinfos);


/*
 * setup_simple_rel_arrays
 *	  准备我们用来快速访问基础关系和 AppendRelInfos 的数组。
 */
void setup_simple_rel_arrays(PlannerInfo *fc_root)
{
	int			fc_size;
	Index		fc_rti;
	ListCell   *fc_lc;

	/* 数组使用 RT 索引 (1..N) 进行访问 */
	fc_size = list_length(fc_root->parse->rtable) + 1;
	fc_root->simple_rel_array_size = fc_size;

	/*
	 * simple_rel_array 初始化为全 NULL，因为还不存在任何 RelOptInfos。
	 * 它将由后续调用 build_simple_rel() 填充。
	 */
	fc_root->simple_rel_array = (RelOptInfo **)
		palloc0(fc_size * sizeof(RelOptInfo *));

	/* simple_rte_array 是 rtable 列表的数组等价物 */
	fc_root->simple_rte_array = (RangeTblEntry **)
		palloc0(fc_size * sizeof(RangeTblEntry *));
	fc_rti = 1;
	foreach(fc_lc, fc_root->parse->rtable)
	{
		RangeTblEntry *fc_rte = (RangeTblEntry *) lfirst(fc_lc);

		fc_root->simple_rte_array[fc_rti++] = fc_rte;
	}

	/* 如果没有 AppendRelInfos，则不需要 append_rel_array */
	if (fc_root->append_rel_list == NIL)
	{
		fc_root->append_rel_array = NULL;
		return;
	}

	fc_root->append_rel_array = (AppendRelInfo **)
		palloc0(fc_size * sizeof(AppendRelInfo *));

	/*
	 * append_rel_array 填充了任何已经存在的 AppendRelInfos，
	 * 目前只能来自 UNION ALL 的扁平化。我们可能会在继承扩展中
	 * 添加更多，但更新数组的责任在于扩展代码。
	 */
	foreach(fc_lc, fc_root->append_rel_list)
	{
		AppendRelInfo *fc_appinfo = lfirst_node(AppendRelInfo, fc_lc);
		int			fc_child_relid = fc_appinfo->child_relid;

		/* 整体检查 */
		Assert(fc_child_relid < fc_size);

		if (fc_root->append_rel_array[fc_child_relid])
			elog(ERROR, "child relation already exists");

		fc_root->append_rel_array[fc_child_relid] = fc_appinfo;
	}
}

/*
 * expand_planner_arrays
 *		通过添加成员的大小扩展 PlannerInfo 的每个 RTE 数组
 *		并将新添加的条目初始化为 NULL
 *
 * 注意：这将导致 append_rel_array 被分配，即使之前没有。
 * 对于当前的使用，这是可以的，因为我们只在添加子关系时调用
 * 这个函数，而子关系总是有 AppendRelInfos。
 */
void expand_planner_arrays(PlannerInfo *fc_root, int add_size)
{
	int			fc_new_size;

	Assert(add_size > 0);

	fc_new_size = fc_root->simple_rel_array_size + add_size;

	fc_root->simple_rel_array = (RelOptInfo **)
		repalloc(fc_root->simple_rel_array,
				 sizeof(RelOptInfo *) * fc_new_size);
	MemSet(fc_root->simple_rel_array + fc_root->simple_rel_array_size,
		   0, sizeof(RelOptInfo *) * add_size);

	fc_root->simple_rte_array = (RangeTblEntry **)
		repalloc(fc_root->simple_rte_array,
				 sizeof(RangeTblEntry *) * fc_new_size);
	MemSet(fc_root->simple_rte_array + fc_root->simple_rel_array_size,
		   0, sizeof(RangeTblEntry *) * add_size);

	if (fc_root->append_rel_array)
	{
		fc_root->append_rel_array = (AppendRelInfo **)
			repalloc(fc_root->append_rel_array,
					 sizeof(AppendRelInfo *) * fc_new_size);
		MemSet(fc_root->append_rel_array + fc_root->simple_rel_array_size,
			   0, sizeof(AppendRelInfo *) * add_size);
	}
	else
	{
		fc_root->append_rel_array = (AppendRelInfo **)
			palloc0(sizeof(AppendRelInfo *) * fc_new_size);
	}

	fc_root->simple_rel_array_size = fc_new_size;
}

/*
 * build_simple_rel
 *	  为基础关系或“其他”关系构造一个新的 RelOptInfo。
 */
RelOptInfo * build_simple_rel(PlannerInfo *fc_root, int fc_relid, RelOptInfo *fc_parent)
{
	RelOptInfo *fc_rel;
	RangeTblEntry *fc_rte;

	/* 关系不应该已经存在 */
	Assert(fc_relid > 0 && fc_relid < fc_root->simple_rel_array_size);
	if (fc_root->simple_rel_array[fc_relid] != NULL)
		elog(ERROR, "rel %d already exists", fc_relid);

	/* 获取关系的 RTE */
	fc_rte = fc_root->simple_rte_array[fc_relid];
	Assert(fc_rte != NULL);

	fc_rel = makeNode(RelOptInfo);
	fc_rel->reloptkind = fc_parent ? RELOPT_OTHER_MEMBER_REL : RELOPT_BASEREL;
	fc_rel->relids = bms_make_singleton(fc_relid);
	fc_rel->rows = 0;
	/* 低廉的启动成本仅在不是要检索所有元组时才有意义 */
	fc_rel->consider_startup = (fc_root->tuple_fraction > 0);
	fc_rel->consider_param_startup = false;	/* 可能会在稍后更改 */
	fc_rel->consider_parallel = false; /* 可能会在稍后更改 */
	fc_rel->reltarget = create_empty_pathtarget();
	fc_rel->pathlist = NIL;
	fc_rel->ppilist = NIL;
	fc_rel->partial_pathlist = NIL;
	fc_rel->cheapest_startup_path = NULL;
	fc_rel->cheapest_total_path = NULL;
	fc_rel->cheapest_unique_path = NULL;
	fc_rel->cheapest_parameterized_paths = NIL;
	fc_rel->relid = fc_relid;
	fc_rel->rtekind = fc_rte->rtekind;
	/* min_attr, max_attr, attr_needed, attr_widths 在下方设置 */
	fc_rel->lateral_vars = NIL;
	fc_rel->indexlist = NIL;
	fc_rel->statlist = NIL;
	fc_rel->pages = 0;
	fc_rel->tuples = 0;
	fc_rel->allvisfrac = 0;
	fc_rel->eclass_indexes = NULL;
	fc_rel->subroot = NULL;
	fc_rel->subplan_params = NIL;
	fc_rel->rel_parallel_workers = -1; /* 在 get_relation_info 中设置 */
	fc_rel->amflags = 0;
	fc_rel->serverid = InvalidOid;
	fc_rel->userid = fc_rte->checkAsUser;
	fc_rel->useridiscurrent = false;
	fc_rel->fdwroutine = NULL;
	fc_rel->fdw_private = NULL;
	fc_rel->unique_for_rels = NIL;
	fc_rel->non_unique_for_rels = NIL;
	fc_rel->baserestrictinfo = NIL;
	fc_rel->baserestrictcost.startup = 0;
	fc_rel->baserestrictcost.per_tuple = 0;
	fc_rel->baserestrict_min_security = UINT_MAX;
	fc_rel->joininfo = NIL;
	fc_rel->has_eclass_joins = false;
	fc_rel->consider_partitionwise_join = false;	/* 可能会在稍后更改 */
	fc_rel->part_scheme = NULL;
	fc_rel->nparts = -1;
	fc_rel->boundinfo = NULL;
	fc_rel->partbounds_merged = false;
	fc_rel->partition_qual = NIL;
	fc_rel->part_rels = NULL;
	fc_rel->live_parts = NULL;
	fc_rel->all_partrels = NULL;
	fc_rel->partexprs = NULL;
	fc_rel->nullable_partexprs = NULL;

	/*
	 * 在继承层次结构中传递各种信息。
	 */
	if (fc_parent)
	{
		/*
		 * 每个直接或间接的子关系都想知道其最顶层父关系的 relids。
		 */
		if (fc_parent->top_parent_relids)
			fc_rel->top_parent_relids = fc_parent->top_parent_relids;
		else
			fc_rel->top_parent_relids = bms_copy(fc_parent->relids);

		/*
		 * 还从 appendrel 父关系传递横向引用信息到其子关系。
		 * 我们故意给每个子关系相同的最小参数化，尽管很有可能
		 * 有些子关系并不引用所有横向关系。这是因为父关系的任何
		 * append 路径都必须为每个子关系具有相同的参数化，
		 * 而强制额外的 reparameterize_path() 调用并没有价值。
		 * 同样，对父关系的横向引用会阻止对每个子关系使用
		 * 其他可移动的连接关系。
		 *
		 * 子关系可能有自己的子关系，在这种情况下
		 * 最顶层父关系的横向信息将一直传播下去。
		 */
		fc_rel->direct_lateral_relids = fc_parent->direct_lateral_relids;
		fc_rel->lateral_relids = fc_parent->lateral_relids;
		fc_rel->lateral_referencers = fc_parent->lateral_referencers;
	}
	else
	{
		fc_rel->top_parent_relids = NULL;
		fc_rel->direct_lateral_relids = NULL;
		fc_rel->lateral_relids = NULL;
		fc_rel->lateral_referencers = NULL;
	}

	/* 检查 rtable 条目的类型 */
	switch (fc_rte->rtekind)
	{
		case RTE_RELATION:
			/* 表 --- 从系统目录中检索统计信息 */
			get_relation_info(fc_root, fc_rte->relid, fc_rte->inh, fc_rel);
			break;
		case RTE_SUBQUERY:
		case RTE_FUNCTION:
		case RTE_TABLEFUNC:
		case RTE_VALUES:
		case RTE_CTE:
		case RTE_NAMEDTUPLESTORE:

			/*
			 * 子查询、函数、表函数、值列表、CTE 或 ENR --- 设置
			 * 属性范围和数组
			 *
			 * 注意：为了支持整个行的变量，范围中包括 0
			 */
			fc_rel->min_attr = 0;
			fc_rel->max_attr = list_length(fc_rte->eref->colnames);
			fc_rel->attr_needed = (Relids *)
				palloc0((fc_rel->max_attr - fc_rel->min_attr + 1) * sizeof(Relids));
			fc_rel->attr_widths = (int32 *)
				palloc0((fc_rel->max_attr - fc_rel->min_attr + 1) * sizeof(int32));
			break;
		case RTE_RESULT:
			/* RTE_RESULT 没有列，也不可能有整个行的变量 */
			fc_rel->min_attr = 0;
			fc_rel->max_attr = -1;
			fc_rel->attr_needed = NULL;
			fc_rel->attr_widths = NULL;
			break;
		default:
			elog(ERROR, "unrecognized RTE kind: %d",
				 (int) fc_rte->rtekind);
			break;
	}

	/*
	 * 将父关系的条件复制到子关系，并进行适当的变量替换。
	 * 如果出现任何常量假的或 NULL 的条款，我们可以立即将子关系标记为
	 * 虚拟的。（我们必须立即这样做，以便在递归
	 * expand_partitioned_rtentry 时修剪能够正确工作。）
	 */
	if (fc_parent)
	{
		AppendRelInfo *fc_appinfo = fc_root->append_rel_array[fc_relid];

		Assert(fc_appinfo != NULL);
		if (!apply_child_basequals(fc_root, fc_parent, fc_rel, fc_rte, fc_appinfo))
		{
			/*
			 * 某些限制条件在替换后简化为常量 FALSE 或 NULL，
			 * 因此该子关系不需要被扫描。
			 */
			mark_dummy_rel(fc_rel);
		}
	}

	/* 将完成的结构保存在查询的 simple_rel_array 中 */
	fc_root->simple_rel_array[fc_relid] = fc_rel;

	return fc_rel;
}

/*
 * find_base_rel
 *	  查找必须已经存在的基础或其他关系条目。
 */
RelOptInfo * find_base_rel(PlannerInfo *fc_root, int fc_relid)
{
	RelOptInfo *fc_rel;

	Assert(fc_relid > 0);

	if (fc_relid < fc_root->simple_rel_array_size)
	{
		fc_rel = fc_root->simple_rel_array[fc_relid];
		if (fc_rel)
			return fc_rel;
	}

	elog(ERROR, "no relation entry for relid %d", fc_relid);

	return NULL;				/* 保持编译器安静 */
}

/*
 * build_join_rel_hash
 *	  为连接关系构造辅助散列表。
 */
static void fc_build_join_rel_hash(PlannerInfo *fc_root)
{
	HTAB	   *fc_hashtab;
	HASHCTL		fc_hash_ctl;
	ListCell   *fc_l;

	/* 创建哈希表 */
	fc_hash_ctl.keysize = sizeof(Relids);
	fc_hash_ctl.entrysize = sizeof(JoinHashEntry);
	fc_hash_ctl.hash = bitmap_hash;
	fc_hash_ctl.match = bitmap_match;
	fc_hash_ctl.hcxt = CurrentMemoryContext;
	fc_hashtab = hash_create("JoinRelHashTable",
						  256L,
						  &fc_hash_ctl,
						  HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_CONTEXT);

	/* 插入所有已经存在的连接关系 */
	foreach(fc_l, fc_root->join_rel_list)
	{
		RelOptInfo *fc_rel = (RelOptInfo *) lfirst(fc_l);
		JoinHashEntry *fc_hentry;
		bool		fc_found;

		fc_hentry = (JoinHashEntry *) hash_search(fc_hashtab,
											   &(fc_rel->relids),
											   HASH_ENTER,
											   &fc_found);
		Assert(!fc_found);
		fc_hentry->join_rel = fc_rel;
	}

	fc_root->join_rel_hash = fc_hashtab;
}

/*
 * find_join_rel
 *	  返回与 'relids'（一组 RT 索引）对应的关系条目，
 *	  如果不存在则返回 NULL。这用于连接关系。
 */
RelOptInfo * find_join_rel(PlannerInfo *fc_root, Relids fc_relids)
{
	/*
	 * 当列表增长“过长”时切换到使用散列查找。
	 * 阈值是任意的，并且仅在这里知道。
	 */
	if (!fc_root->join_rel_hash && list_length(fc_root->join_rel_list) > 32)
		fc_build_join_rel_hash(fc_root);

	/*
	 * 根据需要使用散列表查找或线性搜索。
	 *
	 * 注意：看似多余的 hashkey 变量用于避免获取 relids 的地址；
	 * 除非编译器极其智能，否则这样做会强制 relids 脱离寄存器
	 * 并可能减慢列表搜索的速度。
	 */
	if (fc_root->join_rel_hash)
	{
		Relids		fc_hashkey = fc_relids;
		JoinHashEntry *fc_hentry;

		fc_hentry = (JoinHashEntry *) hash_search(fc_root->join_rel_hash,
											   &fc_hashkey,
											   HASH_FIND,
											   NULL);
		if (fc_hentry)
			return fc_hentry->join_rel;
	}
	else
	{
		ListCell   *fc_l;

		foreach(fc_l, fc_root->join_rel_list)
		{
			RelOptInfo *fc_rel = (RelOptInfo *) lfirst(fc_l);

			if (bms_equal(fc_rel->relids, fc_relids))
				return fc_rel;
		}
	}

	return NULL;
}

/*
 * set_foreign_rel_properties
 *	设置外部连接字段，如果外部和内部关系是属于同一服务器且分配给同一用户的外部表（或连接）以检查访问权限。
 *
 * 除了userid的精确匹配，我们允许一种情况：一侧userid为零（意味着当前用户），而另一侧有显式userid且恰好等于当前用户；但在这种情况下，连接的下推仅对当前用户有效。useridiscurrent字段记录我们是否必须为此连接或任何子连接做出这样的假设。
 *
 * 否则，这些字段将保持无效，因此GetForeignJoinPaths将不会被调用以进行连接关系。
 *
 */
static void set_foreign_rel_properties(RelOptInfo *fc_joinrel, RelOptInfo *fc_outer_rel,
						   RelOptInfo *fc_inner_rel)
{
	if (OidIsValid(fc_outer_rel->serverid) &&
		fc_inner_rel->serverid == fc_outer_rel->serverid)
	{
		if (fc_inner_rel->userid == fc_outer_rel->userid)
		{
			fc_joinrel->serverid = fc_outer_rel->serverid;
			fc_joinrel->userid = fc_outer_rel->userid;
			fc_joinrel->useridiscurrent = fc_outer_rel->useridiscurrent || fc_inner_rel->useridiscurrent;
			fc_joinrel->fdwroutine = fc_outer_rel->fdwroutine;
		}
		else if (!OidIsValid(fc_inner_rel->userid) &&
				 fc_outer_rel->userid == GetUserId())
		{
			fc_joinrel->serverid = fc_outer_rel->serverid;
			fc_joinrel->userid = fc_outer_rel->userid;
			fc_joinrel->useridiscurrent = true;
			fc_joinrel->fdwroutine = fc_outer_rel->fdwroutine;
		}
		else if (!OidIsValid(fc_outer_rel->userid) &&
				 fc_inner_rel->userid == GetUserId())
		{
			fc_joinrel->serverid = fc_outer_rel->serverid;
			fc_joinrel->userid = fc_inner_rel->userid;
			fc_joinrel->useridiscurrent = true;
			fc_joinrel->fdwroutine = fc_outer_rel->fdwroutine;
		}
	}
}

/*
 * add_join_rel
 *	将给定的连接关系添加到给定PlannerInfo中的连接关系列表中。如果有辅助哈希表，也将其添加到该哈希表中。
 */
static void add_join_rel(PlannerInfo *fc_root, RelOptInfo *fc_joinrel)
{
	/* GEQO 要求我们将新连接关系追加到列表的末尾！ */
	fc_root->join_rel_list = lappend(fc_root->join_rel_list, fc_joinrel);

	/* 将其存储到辅助哈希表中（如果有的话）。 */
	if (fc_root->join_rel_hash)
	{
		JoinHashEntry *fc_hentry;
		bool		fc_found;

		fc_hentry = (JoinHashEntry *) hash_search(fc_root->join_rel_hash,
											   &(fc_joinrel->relids),
											   HASH_ENTER,
											   &fc_found);
		Assert(!fc_found);
		fc_hentry->join_rel = fc_joinrel;
	}
}

/*
 * build_join_rel
 *	  返回与两个给定关系的并集相对应的关系条目，如果没有现有的条目，则创建一个新关系条目。
 *
 * 'joinrelids' 是唯一标识连接的Relids集合
 * 'outer_rel' 和 'inner_rel' 是要连接的关系的关系节点
 * 'sjinfo': 连接上下文信息
 * 'restrictlist_ptr': 结果变量。如果不为NULL，*restrictlist_ptr
 *		接收适用于这对可连接关系的RestrictInfo节点列表。
 *
 * restrictlist_ptr使例程的API有点糟糕，但它节省了对restrictlist的重复计算...
 */
RelOptInfo * build_join_rel(PlannerInfo *fc_root,
			   Relids fc_joinrelids,
			   RelOptInfo *fc_outer_rel,
			   RelOptInfo *fc_inner_rel,
			   SpecialJoinInfo *fc_sjinfo,
			   List **fc_restrictlist_ptr)
{
	RelOptInfo *fc_joinrel;
	List	   *fc_restrictlist;

	/* 此函数仅应在父关系之间进行连接时使用。 */
	Assert(!IS_OTHER_REL(fc_outer_rel) && !IS_OTHER_REL(fc_inner_rel));

	/*
	 * 查看我们是否已经为这组基础关系拥有一个连接关系。
	 */
	fc_joinrel = find_join_rel(fc_root, fc_joinrelids);

	if (fc_joinrel)
	{
		/*
		 * 是的，因此我们只需计算这对组件关系的restrictlist。
		 */
		if (fc_restrictlist_ptr)
			*fc_restrictlist_ptr = fc_build_joinrel_restrictlist(fc_root,
														   fc_joinrel,
														   fc_outer_rel,
														   fc_inner_rel);
		return fc_joinrel;
	}

	/*
	 * 不，因此创建一个。
	 */
	fc_joinrel = makeNode(RelOptInfo);
	fc_joinrel->reloptkind = RELOPT_JOINREL;
	fc_joinrel->relids = bms_copy(fc_joinrelids);
	fc_joinrel->rows = 0;
	/* 低廉的启动成本仅在不是要检索所有元组时才有意义 */
	fc_joinrel->consider_startup = (fc_root->tuple_fraction > 0);
	fc_joinrel->consider_param_startup = false;
	fc_joinrel->consider_parallel = false;
	fc_joinrel->reltarget = create_empty_pathtarget();
	fc_joinrel->pathlist = NIL;
	fc_joinrel->ppilist = NIL;
	fc_joinrel->partial_pathlist = NIL;
	fc_joinrel->cheapest_startup_path = NULL;
	fc_joinrel->cheapest_total_path = NULL;
	fc_joinrel->cheapest_unique_path = NULL;
	fc_joinrel->cheapest_parameterized_paths = NIL;
	/* 从子关系初始化direct_lateral_relids；我们将在下面完成它 */
	fc_joinrel->direct_lateral_relids =
		bms_union(fc_outer_rel->direct_lateral_relids,
				  fc_inner_rel->direct_lateral_relids);
	fc_joinrel->lateral_relids = min_join_parameterization(fc_root, fc_joinrel->relids,
														fc_outer_rel, fc_inner_rel);
	fc_joinrel->relid = 0;			/* 表示不是baserel */
	fc_joinrel->rtekind = RTE_JOIN;
	fc_joinrel->min_attr = 0;
	fc_joinrel->max_attr = 0;
	fc_joinrel->attr_needed = NULL;
	fc_joinrel->attr_widths = NULL;
	fc_joinrel->lateral_vars = NIL;
	fc_joinrel->lateral_referencers = NULL;
	fc_joinrel->indexlist = NIL;
	fc_joinrel->statlist = NIL;
	fc_joinrel->pages = 0;
	fc_joinrel->tuples = 0;
	fc_joinrel->allvisfrac = 0;
	fc_joinrel->eclass_indexes = NULL;
	fc_joinrel->subroot = NULL;
	fc_joinrel->subplan_params = NIL;
	fc_joinrel->rel_parallel_workers = -1;
	fc_joinrel->amflags = 0;
	fc_joinrel->serverid = InvalidOid;
	fc_joinrel->userid = InvalidOid;
	fc_joinrel->useridiscurrent = false;
	fc_joinrel->fdwroutine = NULL;
	fc_joinrel->fdw_private = NULL;
	fc_joinrel->unique_for_rels = NIL;
	fc_joinrel->non_unique_for_rels = NIL;
	fc_joinrel->baserestrictinfo = NIL;
	fc_joinrel->baserestrictcost.startup = 0;
	fc_joinrel->baserestrictcost.per_tuple = 0;
	fc_joinrel->baserestrict_min_security = UINT_MAX;
	fc_joinrel->joininfo = NIL;
	fc_joinrel->has_eclass_joins = false;
	fc_joinrel->consider_partitionwise_join = false;	/* 可能会在稍后更改 */
	fc_joinrel->top_parent_relids = NULL;
	fc_joinrel->part_scheme = NULL;
	fc_joinrel->nparts = -1;
	fc_joinrel->boundinfo = NULL;
	fc_joinrel->partbounds_merged = false;
	fc_joinrel->partition_qual = NIL;
	fc_joinrel->part_rels = NULL;
	fc_joinrel->live_parts = NULL;
	fc_joinrel->all_partrels = NULL;
	fc_joinrel->partexprs = NULL;
	fc_joinrel->nullable_partexprs = NULL;

	/* 计算与外部关系相关的信息。 */
	set_foreign_rel_properties(fc_joinrel, fc_outer_rel, fc_inner_rel);

	/*
	 * 创建一个新的tlist，仅包含此连接需要输出的变量（即，高级连接子句或最终输出所需的）。
	 *
	 * 注意：连接关系的tlist顺序将取决于我们最初尝试从哪对外部和内部关系构建它。但是，无论如何内容应该是相同的。
	 */
	fc_build_joinrel_tlist(fc_root, fc_joinrel, fc_outer_rel);
	fc_build_joinrel_tlist(fc_root, fc_joinrel, fc_inner_rel);
	add_placeholders_to_joinrel(fc_root, fc_joinrel, fc_outer_rel, fc_inner_rel);

	/*
	 * add_placeholders_to_joinrel 还负责将这里计算的任何PlaceHolderVars的ph_lateral集合添加到direct_lateral_relids，所以现在我们可以完成计算。这类似于在min_join_parameterization中计算的传递闭包lateral_relids，除了在这里我们*确实*需要考虑添加的PHVs。
	 */
	fc_joinrel->direct_lateral_relids =
		bms_del_members(fc_joinrel->direct_lateral_relids, fc_joinrel->relids);
	if (bms_is_empty(fc_joinrel->direct_lateral_relids))
		fc_joinrel->direct_lateral_relids = NULL;

	/*
	 * 为新连接关系构建restrict和join子句列表。（调用者可能需要也可能不需要restrictlist，但无论如何我都需要它来进行set_joinrel_size_estimates()。）
	 */
	fc_restrictlist = fc_build_joinrel_restrictlist(fc_root, fc_joinrel,
											  fc_outer_rel, fc_inner_rel);
	if (fc_restrictlist_ptr)
		*fc_restrictlist_ptr = fc_restrictlist;
	fc_build_joinrel_joinlist(fc_joinrel, fc_outer_rel, fc_inner_rel);

	/*
	 * 此处也是检查连接关系是否有任何待处理EquivalenceClass连接的正确位置。
	 */
	fc_joinrel->has_eclass_joins = has_relevant_eclass_joinclause(fc_root, fc_joinrel);

	/* 存储分区信息。 */
	fc_build_joinrel_partition_info(fc_joinrel, fc_outer_rel, fc_inner_rel, fc_restrictlist,
								 fc_sjinfo->jointype);

	/*
	 * 设置连接关系的大小估计。
	 */
	set_joinrel_size_estimates(fc_root, fc_joinrel, fc_outer_rel, fc_inner_rel,
							   fc_sjinfo, fc_restrictlist);

	/*
	 * 如果此连接关系可能在并行工作者中被扫描，则设置consider_parallel标志。如果此标志对任何inner_rel或outer_rel为false，则它对连接关系也必须为false。即使两者都为true，目标列表或资格中也可能存在限制并行的表达式。
	 *
	 * 请注意，如果此关系中有超过两个的关系，则它们可以以任何任意方式分配在inner_rel和outer_rel之间。我们假设这无关紧要，因为在构建到此连接关系的过程中，无论我们选择哪个，我们都应该命中所有相同的baserels和joinclauses；因此，无论我们如何到达这里，我们都应该做出相同的决定。
	 */
	if (fc_inner_rel->consider_parallel && fc_outer_rel->consider_parallel &&
		is_parallel_safe(fc_root, (Node *) fc_restrictlist) &&
		is_parallel_safe(fc_root, (Node *) fc_joinrel->reltarget->exprs))
		fc_joinrel->consider_parallel = true;

	/* 将连接关系添加到PlannerInfo中。 */
	add_join_rel(fc_root, fc_joinrel);

	/*
	 * 此外，如果动态规划连接搜索处于活动状态，则将新连接关系添加到适当的子列表中。注意：你可能认为成员数量的Assert应该是相等的，但某些level 1关系可能已经是连接关系，因此我们只能断言<=。
	 */
	if (fc_root->join_rel_level)
	{
		Assert(fc_root->join_cur_level > 0);
		Assert(fc_root->join_cur_level <= bms_num_members(fc_joinrel->relids));
		fc_root->join_rel_level[fc_root->join_cur_level] =
			lappend(fc_root->join_rel_level[fc_root->join_cur_level], fc_joinrel);
	}

	return fc_joinrel;
}


/*
 * build_child_join_rel
 *	  构建表示给定两个子关系之间连接的 RelOptInfo。
 *
 * 'outer_rel' 和 'inner_rel' 是被连接的子关系的 RelOptInfos
 * 'parent_joinrel' 是表示父关系之间连接的 RelOptInfo。新 RelOptInfo 的某些成员是通过翻译此 RelOptInfo 的相应成员生成的
 * 'sjinfo': 子连接上下文信息
 * 'restrictlist': 应用于这对可连接关系的 RestrictInfo 节点列表
 * 'jointype' 是连接类型（内连接、左连接、全连接等）
 */
RelOptInfo * build_child_join_rel(PlannerInfo *fc_root, RelOptInfo *fc_outer_rel,
					 RelOptInfo *fc_inner_rel, RelOptInfo *fc_parent_joinrel,
					 List *fc_restrictlist, SpecialJoinInfo *fc_sjinfo,
					 JoinType fc_jointype)
{
	RelOptInfo *fc_joinrel = makeNode(RelOptInfo);
	AppendRelInfo **fc_appinfos;
	int			fc_nappinfos;

	/* 只有“其他”关系之间的连接才会在这里出现。 */
	Assert(IS_OTHER_REL(fc_outer_rel) && IS_OTHER_REL(fc_inner_rel));

	/* 父连接关系应该设置 consider_partitionwise_join。 */
	Assert(fc_parent_joinrel->consider_partitionwise_join);

	fc_joinrel->reloptkind = RELOPT_OTHER_JOINREL;
	fc_joinrel->relids = bms_union(fc_outer_rel->relids, fc_inner_rel->relids);
	fc_joinrel->rows = 0;
	/* 低廉的启动成本仅在不是要检索所有元组时才有意义 */
	fc_joinrel->consider_startup = (fc_root->tuple_fraction > 0);
	fc_joinrel->consider_param_startup = false;
	fc_joinrel->consider_parallel = false;
	fc_joinrel->reltarget = create_empty_pathtarget();
	fc_joinrel->pathlist = NIL;
	fc_joinrel->ppilist = NIL;
	fc_joinrel->partial_pathlist = NIL;
	fc_joinrel->cheapest_startup_path = NULL;
	fc_joinrel->cheapest_total_path = NULL;
	fc_joinrel->cheapest_unique_path = NULL;
	fc_joinrel->cheapest_parameterized_paths = NIL;
	fc_joinrel->direct_lateral_relids = NULL;
	fc_joinrel->lateral_relids = NULL;
	fc_joinrel->relid = 0;			/* 表示不是baserel */
	fc_joinrel->rtekind = RTE_JOIN;
	fc_joinrel->min_attr = 0;
	fc_joinrel->max_attr = 0;
	fc_joinrel->attr_needed = NULL;
	fc_joinrel->attr_widths = NULL;
	fc_joinrel->lateral_vars = NIL;
	fc_joinrel->lateral_referencers = NULL;
	fc_joinrel->indexlist = NIL;
	fc_joinrel->pages = 0;
	fc_joinrel->tuples = 0;
	fc_joinrel->allvisfrac = 0;
	fc_joinrel->eclass_indexes = NULL;
	fc_joinrel->subroot = NULL;
	fc_joinrel->subplan_params = NIL;
	fc_joinrel->amflags = 0;
	fc_joinrel->serverid = InvalidOid;
	fc_joinrel->userid = InvalidOid;
	fc_joinrel->useridiscurrent = false;
	fc_joinrel->fdwroutine = NULL;
	fc_joinrel->fdw_private = NULL;
	fc_joinrel->baserestrictinfo = NIL;
	fc_joinrel->baserestrictcost.startup = 0;
	fc_joinrel->baserestrictcost.per_tuple = 0;
	fc_joinrel->joininfo = NIL;
	fc_joinrel->has_eclass_joins = false;
	fc_joinrel->consider_partitionwise_join = false;	/* 可能会在稍后更改 */
	fc_joinrel->top_parent_relids = NULL;
	fc_joinrel->part_scheme = NULL;
	fc_joinrel->nparts = -1;
	fc_joinrel->boundinfo = NULL;
	fc_joinrel->partbounds_merged = false;
	fc_joinrel->partition_qual = NIL;
	fc_joinrel->part_rels = NULL;
	fc_joinrel->live_parts = NULL;
	fc_joinrel->all_partrels = NULL;
	fc_joinrel->partexprs = NULL;
	fc_joinrel->nullable_partexprs = NULL;

	fc_joinrel->top_parent_relids = bms_union(fc_outer_rel->top_parent_relids,
										   fc_inner_rel->top_parent_relids);

	/* 计算与外部关系相关的信息。 */
	set_foreign_rel_properties(fc_joinrel, fc_outer_rel, fc_inner_rel);

	/* 计算将 Vars 映射到子关系所需的信息 */
	fc_appinfos = find_appinfos_by_relids(fc_root, fc_joinrel->relids, &fc_nappinfos);

	/* 设置 reltarget 结构 */
	fc_build_child_join_reltarget(fc_root, fc_parent_joinrel, fc_joinrel,
							   fc_nappinfos, fc_appinfos);

	/* 构建连接信息列表。 */
	fc_joinrel->joininfo = (List *) adjust_appendrel_attrs(fc_root,
														(Node *) fc_parent_joinrel->joininfo,
														fc_nappinfos,
														fc_appinfos);

	/*
	 * 在子连接中引用的 Lateral relids 将与在
	 * 父关系中引用的相同。
	 */
	fc_joinrel->direct_lateral_relids = (Relids) bms_copy(fc_parent_joinrel->direct_lateral_relids);
	fc_joinrel->lateral_relids = (Relids) bms_copy(fc_parent_joinrel->lateral_relids);

	/*
	 * 如果父连接关系有待处理的等价类，子连接关系也有。
	 */
	fc_joinrel->has_eclass_joins = fc_parent_joinrel->has_eclass_joins;

	/* 连接的分区之间自身是否分区？ */
	fc_build_joinrel_partition_info(fc_joinrel, fc_outer_rel, fc_inner_rel, fc_restrictlist,
								 fc_jointype);

	/* 如果父级是并行安全的，则子连接关系也是并行安全的。 */
	fc_joinrel->consider_parallel = fc_parent_joinrel->consider_parallel;

	/* 设置子连接关系的大小估算。 */
	set_joinrel_size_estimates(fc_root, fc_joinrel, fc_outer_rel, fc_inner_rel,
							   fc_sjinfo, fc_restrictlist);

	/* 我们只构建一次连接。 */
	Assert(!find_join_rel(fc_root, fc_joinrel->relids));

	/* 将关系添加到 PlannerInfo。 */
	add_join_rel(fc_root, fc_joinrel);

	/*
	 * 我们可能需要与子连接相关的 EquivalenceClass 成员，
	 * 以便可以为其表示排序路径键。与基关系的子关系一样，
	 * 除非有相关的 eclass 连接（这意味着可能存在合并连接）
	 * 或需要排序的路径键，否则我们不应该需要这个。
	 */
	if (fc_joinrel->has_eclass_joins || has_useful_pathkeys(fc_root, fc_parent_joinrel))
		add_child_join_rel_equivalences(fc_root,
										fc_nappinfos, fc_appinfos,
										fc_parent_joinrel, fc_joinrel);

	pfree(fc_appinfos);

	return fc_joinrel;
}

/*
 * min_join_parameterization
 *
 * 确定连接关系的最小可能参数化，即它包含的其他关系集合，
 * 其中包含的 LATERAL 引用。我们将该值保存在连接的 RelOptInfo 中。
 * 此函数从 build_join_rel() 中分离出来，因为 join_is_legal()
 * 需要该值来检查一个潜在的连接。
 */
Relids min_join_parameterization(PlannerInfo *fc_root,
						  Relids fc_joinrelids,
						  RelOptInfo *fc_outer_rel,
						  RelOptInfo *fc_inner_rel)
{
	Relids		fc_result;

	/*
	 * 基本上，我们只需要输入的 lateral_relids 的合集，
	 * 减去已经在连接中的部分。
	 *
	 * 这不是计算结果的明显有效方式，因为这可能看起来像
	 * 忽略了 PlaceholderVars 的可能 lateral 引用，尽管
	 * 这些引用将在连接处计算，但不在任何输入中。
	 * 然而，由于 create_lateral_join_info() 已经针对连接中的
	 * 每个基关系成员收取了所有这些 PHV 引用，因此它们
	 * 将在输入的 lateral_relids 中已被计入。同样，我们
	 * 也不需要担心在这里进行传递闭包，因为这已经在
	 * 原始基关系的 lateral_relids 中被考虑过。
	 */
	fc_result = bms_union(fc_outer_rel->lateral_relids, fc_inner_rel->lateral_relids);
	fc_result = bms_del_members(fc_result, fc_joinrelids);

	/* 保持不变量，如果为空则结果恰好为 NULL */
	if (bms_is_empty(fc_result))
		fc_result = NULL;

	return fc_result;
}

/*
 * build_joinrel_tlist
 *	  从输入关系构建连接关系的目标列表。
 *	  （这调用两次以处理两个输入关系。）
 *
 * 连接的目标列表包括所有其成员关系的 Vars，
 * 这些 Vars 在连接上方依然需要。此子例程将所有
 * 指定输入关系的目标列表中的 Vars 添加到连接关系的目标列表中。
 *
 * 我们还计算连接输出的预期宽度，利用在
 * baserel 级别由 set_rel_width() 缓存的数据。
 */
static void fc_build_joinrel_tlist(PlannerInfo *fc_root, RelOptInfo *fc_joinrel,
					RelOptInfo *fc_input_rel)
{
	Relids		fc_relids = fc_joinrel->relids;
	ListCell   *fc_vars;

	foreach(fc_vars, fc_input_rel->reltarget->exprs)
	{
		Var		   *fc_var = (Var *) lfirst(fc_vars);

		/*
		 * 忽略输入目标列表中的 PlaceholderVars；我们会自行
		 * 决定是否复制它们。
		 */
		if (IsA(fc_var, PlaceHolderVar))
			continue;

		/*
		 * 否则，基关系或连接关系目标列表中的任何内容都应该是
		 * 一个 Var。（更一般的情况只会出现在 appendrel 子
		 * 关系中，这里永远不会看到。）
		 */
		if (!IsA(fc_var, Var))
			elog(ERROR, "unexpected node type in rel targetlist: %d",
				 (int) nodeTag(fc_var));

		if (fc_var->varno == ROWID_VAR)
		{
			/* UPDATE/DELETE/MERGE 行标识 vars 始终需要 */
			RowIdentityVarInfo *fc_ridinfo = (RowIdentityVarInfo *)
			list_nth(fc_root->row_identity_vars, fc_var->varattno - 1);

			fc_joinrel->reltarget->exprs = lappend(fc_joinrel->reltarget->exprs,
												fc_var);
			/* Vars 的成本为零，因此无需调整 reltarget->cost */
			fc_joinrel->reltarget->width += fc_ridinfo->rowidwidth;
		}
		else
		{
			RelOptInfo *fc_baserel;
			int			fc_ndx;

			/* 获取 Var 的原始基关系 */
			fc_baserel = find_base_rel(fc_root, fc_var->varno);

			/* 在此连接关系上方仍然需要吗? */
			fc_ndx = fc_var->varattno - fc_baserel->min_attr;
			if (bms_nonempty_difference(fc_baserel->attr_needed[fc_ndx], fc_relids))
			{
				/* 是的，将其添加到输出中 */
				fc_joinrel->reltarget->exprs = lappend(fc_joinrel->reltarget->exprs,
													fc_var);
				/* Vars 的成本为零，因此无需调整 reltarget->cost */
				fc_joinrel->reltarget->width += fc_baserel->attr_widths[fc_ndx];
			}
		}
	}
}


/*
 * build_joinrel_restrictlist
 * build_joinrel_joinlist
 *	  这些例程从它所连接的关系的 joininfo 列表构建用于连接关系的限制和连接子句列表。
 *
 *	  这些例程是分开的，因为限制列表必须为我们考虑的每对输入子关系重新构建，而连接列表只需为任何连接 RelOptInfo 计算一次。
 *	  连接列表由构成 joinrel 的关系集合完全决定，因此我们应该从任何候选子关系对获得相同的结果（顺序除外）。但是限制列表是任何未在子关系中处理的内容，因此它依赖于考虑哪些子关系。
 *
 *	  如果输入关系中的连接子句引用仍不在 joinrel 中的基础关系，则它仍然是 joinrel 的连接子句；
 *	  我们将其放入 joinrel 的 joininfo 列表中。否则，该子句现在是连接关系的限制子句，我们将其返回给 build_joinrel_restrictlist() 的调用者，以便存储在由这对子关系构成的连接路径中。
 *	  （在连接树的上层不再需要考虑它。）
 *
 *	  在许多情况下，我们会在两个输入关系的连接列表中找到相同的 RestrictInfos，因此要小心消除重复项。
 *	  指针相等性应该是检测重复项的充足测试，因为所有不同的连接列表条目最终引用的都是通过 distribute_restrictinfo_to_rels() 推送到它们的 RestrictInfos。
 *
 * 'joinrel' 是一个连接关系节点
 * 'outer_rel' 和 'inner_rel' 是一对可以连接在一起形成 joinrel 的关系。
 *
 * build_joinrel_restrictlist() 返回相关的 restrictinfos 列表，
 * 而 build_joinrel_joinlist() 则将其结果存储在 joinrel 的 joininfo 列表中。
 * 必须有一个或另一个接受给定的每个子句！
 *
 * 注意：以前，我们对每个输入 RestrictInfo 进行了深拷贝以传递给连接关系。我相信这不再必要，因为 RestrictInfo 节点不再依赖于上下文。
 * 相反，只需将原始节点包含在为连接关系制作的列表中。
 */
static List * fc_build_joinrel_restrictlist(PlannerInfo *fc_root,
						   RelOptInfo *fc_joinrel,
						   RelOptInfo *fc_outer_rel,
						   RelOptInfo *fc_inner_rel)
{
	List	   *fc_result;

	/*
	 * 收集所有在此级别语法上属于的子句，消除任何重复项（重要，因为我们将看到来自两个输入关系的许多相同子句）。
	 */
	fc_result = fc_subbuild_joinrel_restrictlist(fc_joinrel, fc_outer_rel->joininfo, NIL);
	fc_result = fc_subbuild_joinrel_restrictlist(fc_joinrel, fc_inner_rel->joininfo, fc_result);

	/*
	 * 添加从等价类派生的任何子句。这些子句不能与 joininfo 列表中的子句重复，因此不必检查。
	 */
	fc_result = list_concat(fc_result,
						 generate_join_implied_equalities(fc_root,
														  fc_joinrel->relids,
														  fc_outer_rel->relids,
														  fc_inner_rel));

	return fc_result;
}

static void fc_build_joinrel_joinlist(RelOptInfo *fc_joinrel,
					   RelOptInfo *fc_outer_rel,
					   RelOptInfo *fc_inner_rel)
{
	List	   *fc_result;

	/*
	 * 收集所有语法上属于此级别以上的子句，消除任何重复项（重要，因为我们将看到来自两个输入关系的许多相同子句）。
	 */
	fc_result = fc_subbuild_joinrel_joinlist(fc_joinrel, fc_outer_rel->joininfo, NIL);
	fc_result = fc_subbuild_joinrel_joinlist(fc_joinrel, fc_inner_rel->joininfo, fc_result);

	fc_joinrel->joininfo = fc_result;
}

static List * fc_subbuild_joinrel_restrictlist(RelOptInfo *fc_joinrel,
							  List *fc_joininfo_list,
							  List *fc_new_restrictlist)
{
	ListCell   *fc_l;

	foreach(fc_l, fc_joininfo_list)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_l);

		if (bms_is_subset(fc_rinfo->required_relids, fc_joinrel->relids))
		{
			/*
			 * 该子句成为 joinrel 的限制子句，因为它不引用任何外部关系。将其添加到列表中，注意消除重复项。（由于不同连接列表中的 RestrictInfo 节点将被多重链接而不是复制，因此指针相等性应该是一个足够的测试。）
			 */
			fc_new_restrictlist = list_append_unique_ptr(fc_new_restrictlist, fc_rinfo);
		}
		else
		{
			/*
			 * 该子句在此级别仍然是连接子句，因此我们在这个例程中忽略它。
			 */
		}
	}

	return fc_new_restrictlist;
}

static List * fc_subbuild_joinrel_joinlist(RelOptInfo *fc_joinrel,
						  List *fc_joininfo_list,
						  List *fc_new_joininfo)
{
	ListCell   *fc_l;

	/* 预计仅在父关系之间的连接中调用。 */
	Assert(fc_joinrel->reloptkind == RELOPT_JOINREL);

	foreach(fc_l, fc_joininfo_list)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_l);

		if (bms_is_subset(fc_rinfo->required_relids, fc_joinrel->relids))
		{
			/*
			 * 该子句成为 joinrel 的限制子句，因为它不引用任何外部关系。因此，我们可以在这个例程中忽略它。
			 */
		}
		else
		{
			/*
			 * 该子句在此级别仍然是连接子句，因此将其添加到新的 joininfo 列表中，注意消除重复项。
			 * （由于不同连接列表中的 RestrictInfo 节点将被多重链接而不是复制，因此指针相等性应该是一个足够的测试。）
			 */
			fc_new_joininfo = list_append_unique_ptr(fc_new_joininfo, fc_rinfo);
		}
	}

	return fc_new_joininfo;
}


/*
 * fetch_upper_rel
 *		构建一个描述一些扫描/连接查询处理的 RelOptInfo，或者如果有人已经构建了它，则返回一个现有的。
 *
 * 一个“上层”关系由 UpperRelationKind 和 Relids 集合标识。
 * Relids 集合的含义在此未指定，并且很可能会因不同的关系类型而有所不同。
 *
 * 上层 RelOptInfo 中的大多数字段未使用，并且在此不会设置（尽管 makeNode 应该确保它们为零）。我们基本上只关心对于 add_path() 和 set_cheapest() 感兴趣的字段。
 */
RelOptInfo * fetch_upper_rel(PlannerInfo *fc_root, UpperRelationKind fc_kind, Relids fc_relids)
{
	RelOptInfo *fc_upperrel;
	ListCell   *fc_lc;

	/*
	 * 目前，我们的索引数据结构只是每种关系类型的一个列表。
	 * 如果我们有太多某种类型的情况而导致其停止良好运作，我们可以改进它。
	 * 此函数外部的代码不应假设如何找到特定的 upperrel。
	 */

	/* 如果我们已经为该查询创建了 upperrel，则返回它 */
	foreach(fc_lc, fc_root->upper_rels[fc_kind])
	{
		fc_upperrel = (RelOptInfo *) lfirst(fc_lc);

		if (bms_equal(fc_upperrel->relids, fc_relids))
			return fc_upperrel;
	}

	fc_upperrel = makeNode(RelOptInfo);
	fc_upperrel->reloptkind = RELOPT_UPPER_REL;
	fc_upperrel->relids = bms_copy(fc_relids);

	/* 低廉的启动成本仅在不是要检索所有元组时才有意义 */
	fc_upperrel->consider_startup = (fc_root->tuple_fraction > 0);
	fc_upperrel->consider_param_startup = false;
	fc_upperrel->consider_parallel = false;	/* 可能会在稍后更改 */
	fc_upperrel->reltarget = create_empty_pathtarget();
	fc_upperrel->pathlist = NIL;
	fc_upperrel->cheapest_startup_path = NULL;
	fc_upperrel->cheapest_total_path = NULL;
	fc_upperrel->cheapest_unique_path = NULL;
	fc_upperrel->cheapest_parameterized_paths = NIL;

	fc_root->upper_rels[fc_kind] = lappend(fc_root->upper_rels[fc_kind], fc_upperrel);

	return fc_upperrel;
}



/*
 * find_childrel_parents
 * 计算一个附加关系子关系的父关系 ID 集合。
 *
 * 由于附加关系可以嵌套，子关系可能有多个级别的附加关系祖先。此函数计算所有父关系 ID 的 Relids 集合。
 */
Relids find_childrel_parents(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	Relids		fc_result = NULL;

	Assert(fc_rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
	Assert(fc_rel->relid > 0 && fc_rel->relid < fc_root->simple_rel_array_size);

	do
	{
		AppendRelInfo *fc_appinfo = fc_root->append_rel_array[fc_rel->relid];
		Index		fc_prelid = fc_appinfo->parent_relid;

		fc_result = bms_add_member(fc_result, fc_prelid);

		/* 向上遍历到父关系，如果它也是子关系则循环 */
		fc_rel = find_base_rel(fc_root, fc_prelid);
	} while (fc_rel->reloptkind == RELOPT_OTHER_MEMBER_REL);

	Assert(fc_rel->reloptkind == RELOPT_BASEREL);

	return fc_result;
}


/*
 * get_baserel_parampathinfo
 * 获取基本关系的参数路径的 ParamPathInfo，如果尚未存在则构造一个。
 *
 * 这集中估算参数路径的行数。我们需要缓存这些信息，以确保对于给定关系的所有相同参数化的路径使用相同的行数。这里也是确定参数化路径将负责评估哪些可移动连接条件的方便地点。
 */
ParamPathInfo * get_baserel_parampathinfo(PlannerInfo *fc_root, RelOptInfo *fc_baserel,
						  Relids fc_required_outer)
{
	ParamPathInfo *fc_ppi;
	Relids		fc_joinrelids;
	List	   *fc_pclauses;
	List	   *fc_eqclauses;
	double		fc_rows;
	ListCell   *fc_lc;

	/* 如果 rel 具有 LATERAL 引用，则它的每条路径都应考虑这些引用 */
	Assert(bms_is_subset(fc_baserel->lateral_relids, fc_required_outer));

	/* 非参数化路径没有 ParamPathInfo */
	if (bms_is_empty(fc_required_outer))
		return NULL;

	Assert(!bms_overlap(fc_baserel->relids, fc_required_outer));

	/* 如果我们已经有了这个参数化的 PPI，就直接返回它 */
	if ((fc_ppi = find_param_path_info(fc_baserel, fc_required_outer)))
		return fc_ppi;

	/*
	 * 识别给定此参数化可以移至该基础关系的所有连接子句。
	 */
	fc_joinrelids = bms_union(fc_baserel->relids, fc_required_outer);
	fc_pclauses = NIL;
	foreach(fc_lc, fc_baserel->joininfo)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

		if (join_clause_is_movable_into(fc_rinfo,
										fc_baserel->relids,
										fc_joinrelids))
			fc_pclauses = lappend(fc_pclauses, fc_rinfo);
	}

	/*
	 * 也添加由等价类生成的连接子句。原则上，这些应该始终满足
	 * join_clause_is_movable_into；但如果我们在外连接下，子句可能包含
	 * 变量，而这些变量只有在连接上方才能被评估，因此我们必须检查。
	 */
	fc_eqclauses = generate_join_implied_equalities(fc_root,
												 fc_joinrelids,
												 fc_required_outer,
												 fc_baserel);
	foreach(fc_lc, fc_eqclauses)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

		if (join_clause_is_movable_into(fc_rinfo,
										fc_baserel->relids,
										fc_joinrelids))
			fc_pclauses = lappend(fc_pclauses, fc_rinfo);
	}

	/* 估计参数化扫描返回的行数 */
	fc_rows = get_parameterized_baserel_size(fc_root, fc_baserel, fc_pclauses);

	/* 现在我们可以构建 ParamPathInfo */
	fc_ppi = makeNode(ParamPathInfo);
	fc_ppi->ppi_req_outer = fc_required_outer;
	fc_ppi->ppi_rows = fc_rows;
	fc_ppi->ppi_clauses = fc_pclauses;
	fc_baserel->ppilist = lappend(fc_baserel->ppilist, fc_ppi);

	return fc_ppi;
}

/*
 * get_joinrel_parampathinfo
 *		获取连接关系的参数化路径的 ParamPathInfo，
 *		如果我们还没有，那么构建一个。
 *
 * 这集中估计参数化路径的行数。
 * 我们需要缓存这些，以确保对给定关系的所有相同参数化路径使用相同的行数。
 * 这也是确定参数化路径将负责评估哪些可移动连接子句的方便之处。
 *
 * outer_path 和 inner_path 是一对可以用于构造连接的输入路径，
 * restrict_clauses 是必须在使用这些输入时在连接节点应用的常规连接子句列表
 * （包括从 EquivalenceClasses 导出的子句）。
 *
 * 与基础关系的情况不同，可在连接处强制执行的可移动连接子句集
 * 随着所选输入路径对的变化而变化，所以我们必须计算它并返回，即使我们已经有一个匹配的
 * ParamPathInfo。我们通过将移动到此连接的任何子句添加到 *restrict_clauses 来处理这个问题，
 * 该参数是一个输入/输出参数。 （添加的方式不会修改传入的 List 结构。）
 *
 * 注意：在考虑嵌套循环连接时，调用者必须从 restrict_clauses 中移除
 * 任何计划被推送到右侧路径的可移动子句。
 * 我们在这里不这样做，因为对于其他连接类型来说这是不必要的。
 */
ParamPathInfo * get_joinrel_parampathinfo(PlannerInfo *fc_root, RelOptInfo *fc_joinrel,
						  Path *fc_outer_path,
						  Path *fc_inner_path,
						  SpecialJoinInfo *fc_sjinfo,
						  Relids fc_required_outer,
						  List **fc_restrict_clauses)
{
	ParamPathInfo *fc_ppi;
	Relids		fc_join_and_req;
	Relids		fc_outer_and_req;
	Relids		fc_inner_and_req;
	List	   *fc_pclauses;
	List	   *fc_eclauses;
	List	   *fc_dropped_ecs;
	double		fc_rows;
	ListCell   *fc_lc;

	/* 如果 rel 具有 LATERAL 引用，则它的每条路径都应考虑这些引用 */
	Assert(bms_is_subset(fc_joinrel->lateral_relids, fc_required_outer));

	/* 非参数化路径没有 ParamPathInfo 或额外的连接子句 */
	if (bms_is_empty(fc_required_outer))
		return NULL;

	Assert(!bms_overlap(fc_joinrel->relids, fc_required_outer));

	/*
	 * 确定根据此参数化，可以移动到此连接关系的所有连接子句。
	 * 这些是可以移动到此连接的子句，但不能移动到任何输入路径中。
	 * 将非参数化输入路径视为不接受参数化子句（因为它不会，
	 * 根据上述快捷方式退出），即使连接子句运动规则
	 * 可能允许将相同的子句移动到该关系的参数化路径中。
	 */
	fc_join_and_req = bms_union(fc_joinrel->relids, fc_required_outer);
	if (fc_outer_path->param_info)
		fc_outer_and_req = bms_union(fc_outer_path->parent->relids,
								  PATH_REQ_OUTER(fc_outer_path));
	else
		fc_outer_and_req = NULL;	/* 外部路径不接受参数 */
	if (fc_inner_path->param_info)
		fc_inner_and_req = bms_union(fc_inner_path->parent->relids,
								  PATH_REQ_OUTER(fc_inner_path));
	else
		fc_inner_and_req = NULL;	/* 内部路径不接受参数 */

	fc_pclauses = NIL;
	foreach(fc_lc, fc_joinrel->joininfo)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

		if (join_clause_is_movable_into(fc_rinfo,
										fc_joinrel->relids,
										fc_join_and_req) &&
			!join_clause_is_movable_into(fc_rinfo,
										 fc_outer_path->parent->relids,
										 fc_outer_and_req) &&
			!join_clause_is_movable_into(fc_rinfo,
										 fc_inner_path->parent->relids,
										 fc_inner_and_req))
			fc_pclauses = lappend(fc_pclauses, fc_rinfo);
	}

	/* 也考虑由 EquivalenceClasses 生成的连接子句 */
	fc_eclauses = generate_join_implied_equalities(fc_root,
												fc_join_and_req,
												fc_required_outer,
												fc_joinrel);
	/* 我们只想要那些不可移动到更低级别的子句 */
	fc_dropped_ecs = NIL;
	foreach(fc_lc, fc_eclauses)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

		/*
		 * 原则上，join_clause_is_movable_into() 应该接受由
		 * generate_join_implied_equalities() 返回的任何内容；但是因为它的
		 * 分析只是大致的，有时它并不成立。因此我们当前不能使用这个 Assert；
		 * 而是只是假定在这个层次应用连接子句是可以的。
		 */
#ifdef NOT_USED
		Assert(join_clause_is_movable_into(fc_rinfo,
										   fc_joinrel->relids,
										   fc_join_and_req));
#endif
		if (join_clause_is_movable_into(fc_rinfo,
										fc_outer_path->parent->relids,
										fc_outer_and_req))
			continue;			/* 如果可移动到 LHS，则丢弃 */
		if (join_clause_is_movable_into(fc_rinfo,
										fc_inner_path->parent->relids,
										fc_inner_and_req))
		{
			/* 如果可移动到 RHS，则丢弃，但记得 EC 以便下面使用 */
			Assert(fc_rinfo->left_ec == fc_rinfo->right_ec);
			fc_dropped_ecs = lappend(fc_dropped_ecs, fc_rinfo->left_ec);
			continue;
		}
		fc_pclauses = lappend(fc_pclauses, fc_rinfo);
	}

	/*
	 * EquivalenceClasses 比我们所希望的更难处理，因为
	 * 给定 EC 可能会根据上下文生成不同的子句。
	 * 假设我们有一个 EC {X.X, Y.Y, Z.Z}，其中 X 和 Y 是当前连接的 LHS 和 RHS，
	 * Z 在 required_outer 中，进一步假设 inner_path 由 X 和 Z 参数化。
	 * 上面的代码将从该 EC 生成 Z.Z = X.X 或 Z.Z = Y.Y，
	 * 在后一种情况下将其丢弃为可移动到 RHS。
	 * 然而，EC 机制可能在 inner_path 中生成 Y.Y = X.X 或
	 * Y.Y = Z.Z 作为 EC 强制子句；它不会生成两者，我们也无法从这里
	 * 清楚地判断它选择了哪一个。如果我们不在这个连接中添加任何子句，
	 * 我们将面临 EC 缺乏强制的情况；Z.Z 或 X.X 将无法被约束为与
	 * EC 的其他成员相等。 （当我们将 Z 连接到这个 X/Y 路径时，
	 * 我们肯定会丢弃在该连接生成的任何 EC 子句，因此这个遗漏不会在后面修正。）
	 *
	 * 为了解决这个问题，对于我们丢弃这样的子句的每个 EC，尝试生成一个子句
	 * 连接 required_outer 关系到连接的 LHS
	 * （在上述示例中为 “Z.Z = X.X”）。如果成功，并且如果
	 * 子句不能移到 LHS，将其添加到当前连接的限制子句中。
	 * （如果一个 EC 不能生成这样的子句，那么它在这里没有需要强制的内容，
	 * 而如果子句可以移动到 LHS，则它应该在该路径中被强制。）
	 *
	 * 注意，我们不需要对子句被视为可移动到 LHS 的 EC 进行类似处理，
	 * 因为 LHS 不能引用 RHS，因此对于它可能实际内部强制的内容，
	 * 不存在可比的模糊性。
	 */
	if (fc_dropped_ecs)
	{
		Relids		fc_real_outer_and_req;

		fc_real_outer_and_req = bms_union(fc_outer_path->parent->relids,
									   fc_required_outer);
		fc_eclauses =
			generate_join_implied_equalities_for_ecs(fc_root,
													 fc_dropped_ecs,
													 fc_real_outer_and_req,
													 fc_required_outer,
													 fc_outer_path->parent);
		foreach(fc_lc, fc_eclauses)
		{
			RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

			/* 如上，无法在这里断言 */
#ifdef NOT_USED
			Assert(join_clause_is_movable_into(fc_rinfo,
											   fc_outer_path->parent->relids,
											   fc_real_outer_and_req));
#endif
			if (!join_clause_is_movable_into(fc_rinfo,
											 fc_outer_path->parent->relids,
											 fc_outer_and_req))
				fc_pclauses = lappend(fc_pclauses, fc_rinfo);
		}
	}

	
/*
	 * 现在，将识别出的下移子句附加到调用者的
	 * restrict_clauses 列表中。通过以这种顺序使用 list_concat，我们可以保持 
	 * restrict_clauses 的原始列表结构不受损害。
	 */
	*fc_restrict_clauses = list_concat(fc_pclauses, *fc_restrict_clauses);

	/* 如果我们已经有了这个参数化的 PPI，就直接返回它 */
	if ((fc_ppi = find_param_path_info(fc_joinrel, fc_required_outer)))
		return fc_ppi;

	/* 估计参数化连接返回的行数 */
	fc_rows = get_parameterized_joinrel_size(fc_root, fc_joinrel,
										  fc_outer_path,
										  fc_inner_path,
										  fc_sjinfo,
										  *fc_restrict_clauses);

	/*
	 * 现在我们可以构建 ParamPathInfo。然而，保存
	 * 输入对相关的子句列表是没有意义的。
	 *
	 * 注意：在 GEQO 模式下，我们将在临时内存上下文中被调用，但
	 * joinrel 结构也在那里，所以没有问题。
	 */
	fc_ppi = makeNode(ParamPathInfo);
	fc_ppi->ppi_req_outer = fc_required_outer;
	fc_ppi->ppi_rows = fc_rows;
	fc_ppi->ppi_clauses = NIL;
	fc_joinrel->ppilist = lappend(fc_joinrel->ppilist, fc_ppi);

	return fc_ppi;
}

/*
 * get_appendrel_parampathinfo
 *		获取追加关系的参数化路径的 ParamPathInfo。
 *
 * 对于追加关系，行数估计将只是其子级的
 * 估计的总和。然而，我们仍然需要一个 ParamPathInfo
 * 来标记这个路径需要参数。因此，这只是创建了一个
 * 适当的结构，pppi_rows 为零（并且没有 ppi_clauses，因为
 * Append 节点不负责检查资格）。
 */
ParamPathInfo * get_appendrel_parampathinfo(RelOptInfo *fc_appendrel, Relids fc_required_outer)
{
	ParamPathInfo *fc_ppi;

	/* 如果 rel 具有 LATERAL 引用，则它的每条路径都应考虑这些引用 */
	Assert(bms_is_subset(fc_appendrel->lateral_relids, fc_required_outer));

	/* 非参数化路径没有 ParamPathInfo */
	if (bms_is_empty(fc_required_outer))
		return NULL;

	Assert(!bms_overlap(fc_appendrel->relids, fc_required_outer));

	/* 如果我们已经有了这个参数化的 PPI，就直接返回它 */
	if ((fc_ppi = find_param_path_info(fc_appendrel, fc_required_outer)))
		return fc_ppi;

	/* 否则构建 ParamPathInfo */
	fc_ppi = makeNode(ParamPathInfo);
	fc_ppi->ppi_req_outer = fc_required_outer;
	fc_ppi->ppi_rows = 0;
	fc_ppi->ppi_clauses = NIL;
	fc_appendrel->ppilist = lappend(fc_appendrel->ppilist, fc_ppi);

	return fc_ppi;
}

/*
 * 如果在给定关系中已经可用，则返回由 required_outer 给出的参数化的 ParamPathInfo。
 * 否则返回 NULL。
 */
ParamPathInfo * find_param_path_info(RelOptInfo *fc_rel, Relids fc_required_outer)
{
	ListCell   *fc_lc;

	foreach(fc_lc, fc_rel->ppilist)
	{
		ParamPathInfo *fc_ppi = (ParamPathInfo *) lfirst(fc_lc);

		if (bms_equal(fc_ppi->ppi_req_outer, fc_required_outer))
			return fc_ppi;
	}

	return NULL;
}

/*
 * build_joinrel_partition_info
 *		检查被连接的两个关系是否可以使用分区连接
 *		如果可以，则初始化结果的分区连接关系的分区信息。
 */
static void fc_build_joinrel_partition_info(RelOptInfo *fc_joinrel, RelOptInfo *fc_outer_rel,
							 RelOptInfo *fc_inner_rel, List *fc_restrictlist,
							 JoinType fc_jointype)
{
	PartitionScheme fc_part_scheme;

	/* 如果禁用了分区连接技术，则无事可做。 */
	if (!enable_partitionwise_join)
	{
		Assert(!IS_PARTITIONED_REL(fc_joinrel));
		return;
	}

	/*
	 * 我们只能将此连接视为进一步分区连接的输入，前提是 (a) 输入关系是分区的并且
	 * consider_partitionwise_join=true， (b) 分区方案匹配，和
	 * (c) 我们可以识别分区键之间的等值连接。请注意，如果有可能
	 * have_partkey_equi_join 返回相同的连接关系对不同的答案
	 * 取决于我们首先尝试的连接顺序，这种逻辑将会失效。然而，不应该发生这种情况，
	 * 因为查询规划器推导隐含等式和重新排序连接的方式。请查看 optimizer/README 获取详细信息。
	 */
	if (fc_outer_rel->part_scheme == NULL || fc_inner_rel->part_scheme == NULL ||
		!fc_outer_rel->consider_partitionwise_join ||
		!fc_inner_rel->consider_partitionwise_join ||
		fc_outer_rel->part_scheme != fc_inner_rel->part_scheme ||
		!fc_have_partkey_equi_join(fc_joinrel, fc_outer_rel, fc_inner_rel,
								fc_jointype, fc_restrictlist))
	{
		Assert(!IS_PARTITIONED_REL(fc_joinrel));
		return;
	}

	fc_part_scheme = fc_outer_rel->part_scheme;

	/*
	 * 此函数每个 joinrel 只会被调用一次，因此它
	 * 不应该已经填充分区字段。
	 */
	Assert(!fc_joinrel->part_scheme && !fc_joinrel->partexprs &&
		   !fc_joinrel->nullable_partexprs && !fc_joinrel->part_rels &&
		   !fc_joinrel->boundinfo);

	/*
	 * 如果连接关系是分区的，它使用与连接关系
	 * 相同的分区方案。
	 *
	 * 注意：我们在 try_partitionwise_join() 中计算
	 * 分区边界、分区数量和连接关系的子连接。
	 */
	fc_joinrel->part_scheme = fc_part_scheme;
	set_joinrel_partition_key_exprs(fc_joinrel, fc_outer_rel, fc_inner_rel, fc_jointype);

	/*
	 * 设置 consider_partitionwise_join 标志。
	 */
	Assert(fc_outer_rel->consider_partitionwise_join);
	Assert(fc_inner_rel->consider_partitionwise_join);
	fc_joinrel->consider_partitionwise_join = true;
}

/*
 * have_partkey_equi_join
 *
 * 如果在所有被连接关系的匹配分区键对中存在等值连接条件，则返回真。
 */
static bool fc_have_partkey_equi_join(RelOptInfo *fc_joinrel,
					   RelOptInfo *fc_rel1, RelOptInfo *fc_rel2,
					   JoinType fc_jointype, List *fc_restrictlist)
{
	PartitionScheme fc_part_scheme = fc_rel1->part_scheme;
	ListCell   *fc_lc;
	int			fc_cnt_pks;
	bool		fc_pk_has_clause[PARTITION_MAX_KEYS];
	bool		fc_strict_op;

	/*
	 * 该函数只能在被连接关系具有相同分区方案时调用。
	 */
	Assert(fc_rel1->part_scheme == fc_rel2->part_scheme);
	Assert(fc_part_scheme);

	memset(fc_pk_has_clause, 0, sizeof(fc_pk_has_clause));
	foreach(fc_lc, fc_restrictlist)
	{
		RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_lc);
		OpExpr	   *fc_opexpr;
		Expr	   *fc_expr1;
		Expr	   *fc_expr2;
		int			fc_ipk1;
		int			fc_ipk2;

		/* 如果处理外连接，则仅使用它自己的连接子句。 */
		if (IS_OUTER_JOIN(fc_jointype) &&
			RINFO_IS_PUSHED_DOWN(fc_rinfo, fc_joinrel->relids))
			continue;

		/* 跳过无法用于连接的子句。 */
		if (!fc_rinfo->can_join)
			continue;

		/* 跳过不为相等条件的子句。 */
		if (!fc_rinfo->mergeopfamilies && !OidIsValid(fc_rinfo->hashjoinoperator))
			continue;

		/* 可以假设它是一个 OpExpr。 */
		fc_opexpr = castNode(OpExpr, fc_rinfo->clause);

		/* 匹配操作数与关系。 */
		if (bms_is_subset(fc_rinfo->left_relids, fc_rel1->relids) &&
			bms_is_subset(fc_rinfo->right_relids, fc_rel2->relids))
		{
			fc_expr1 = linitial(fc_opexpr->args);
			fc_expr2 = lsecond(fc_opexpr->args);
		}
		else if (bms_is_subset(fc_rinfo->left_relids, fc_rel2->relids) &&
				 bms_is_subset(fc_rinfo->right_relids, fc_rel1->relids))
		{
			fc_expr1 = lsecond(fc_opexpr->args);
			fc_expr2 = linitial(fc_opexpr->args);
		}
		else
			continue;

		/*
		 * 现在我们需要知道连接操作符是否是严格的；请参阅
		 * pathnodes.h 中的注释。
		 */
		fc_strict_op = op_strict(fc_opexpr->opno);

		/*
		 * 只有引用分区键的子句对分区连接是有用的。
		 */
		fc_ipk1 = fc_match_expr_to_partition_keys(fc_expr1, fc_rel1, fc_strict_op);
		if (fc_ipk1 < 0)
			continue;
		fc_ipk2 = fc_match_expr_to_partition_keys(fc_expr2, fc_rel2, fc_strict_op);
		if (fc_ipk2 < 0)
			continue;

		/*
		 * 如果子句涉及不同序数位置的键，则不能用于分区连接。
		 */
		if (fc_ipk1 != fc_ipk2)
			continue;

		/* 如果分区键的排序与子句的不同，则拒绝。 */
		if (fc_rel1->part_scheme->partcollation[fc_ipk1] != fc_opexpr->inputcollid)
			return false;

		/*
		 * 只有当子句使用与分区键指定的相同操作符族时，
		 * 才允许分区连接。
		 */
		if (fc_rel1->part_scheme->strategy == PARTITION_STRATEGY_HASH)
		{
			if (!OidIsValid(fc_rinfo->hashjoinoperator) ||
				!op_in_opfamily(fc_rinfo->hashjoinoperator,
								fc_part_scheme->partopfamily[fc_ipk1]))
				continue;
		}
		else if (!list_member_oid(fc_rinfo->mergeopfamilies,
								  fc_part_scheme->partopfamily[fc_ipk1]))
			continue;

		/* 将分区键标记为具有等值连接的子句。 */
		fc_pk_has_clause[fc_ipk1] = true;
	}

	/* 检查每个分区键是否具有等值连接条件。 */
	for (fc_cnt_pks = 0; fc_cnt_pks < fc_part_scheme->partnatts; fc_cnt_pks++)
	{
		if (!fc_pk_has_clause[fc_cnt_pks])
			return false;
	}

	return true;
}

/*
 * match_expr_to_partition_keys
 *
 * 尝试将表达式匹配到"rel"的可空或不可空分区键之一。 
 * 返回匹配键的序号位置，如果表达式无法匹配到任何键则返回 -1。
 *
 * strict_op 必须为真，如果表达式将使用严格操作符与分区键进行比较。
 * 这使我们能够同时考虑可空和不可空的分区键。
 */
static int fc_match_expr_to_partition_keys(Expr *fc_expr, RelOptInfo *fc_rel, bool fc_strict_op)
{
	int			fc_cnt;

	/* 此函数仅应在分区关系上调用。 */
	Assert(fc_rel->part_scheme);
	Assert(fc_rel->partexprs);
	Assert(fc_rel->nullable_partexprs);

	/* 移除任何重标记装饰。 */
	while (IsA(fc_expr, RelabelType))
		fc_expr = (Expr *) (castNode(RelabelType, fc_expr))->arg;

	for (fc_cnt = 0; fc_cnt < fc_rel->part_scheme->partnatts; fc_cnt++)
	{
		ListCell   *fc_lc;

		/* 我们始终可以匹配到不可空的分区键。 */
		foreach(fc_lc, fc_rel->partexprs[fc_cnt])
		{
			if (equal(lfirst(fc_lc), fc_expr))
				return fc_cnt;
		}

		if (!fc_strict_op)
			continue;

		/*
		 * 如果它是一个严格连接操作符，则一侧的NULL分区键不会连接到另一侧的任何分区键，
		 * 特别是这样的行不能连接到来自另一侧不同分区的行。因此，搜索可空的分区键也是可以的。
		 */
		foreach(fc_lc, fc_rel->nullable_partexprs[fc_cnt])
		{
			if (equal(lfirst(fc_lc), fc_expr))
				return fc_cnt;
		}
	}

	return -1;
}

/*
 * set_joinrel_partition_key_exprs
 *		为分区连接关系初始化分区键表达式。
 */
static void set_joinrel_partition_key_exprs(RelOptInfo *fc_joinrel,
								RelOptInfo *fc_outer_rel, RelOptInfo *fc_inner_rel,
								JoinType fc_jointype)
{
	PartitionScheme fc_part_scheme = fc_joinrel->part_scheme;
	int			fc_partnatts = fc_part_scheme->partnatts;

	fc_joinrel->partexprs = (List **) palloc0(sizeof(List *) * fc_partnatts);
	fc_joinrel->nullable_partexprs =
		(List **) palloc0(sizeof(List *) * fc_partnatts);

	/*
	 * joinrel的分区表达式与输入关系的相同，
	 * 但我们必须在joinrel的输出中适当分类它们为可空或不可空。
	 * （此外，如果是FULL JOIN，我们还添加一些更多的分区表达式。）
	 */
	for (int fc_cnt = 0; fc_cnt < fc_partnatts; fc_cnt++)
	{
		/* 将这些标记为const以确保我们正确复制它们 */
		const List *fc_outer_expr = fc_outer_rel->partexprs[fc_cnt];
		const List *fc_outer_null_expr = fc_outer_rel->nullable_partexprs[fc_cnt];
		const List *fc_inner_expr = fc_inner_rel->partexprs[fc_cnt];
		const List *fc_inner_null_expr = fc_inner_rel->nullable_partexprs[fc_cnt];
		List	   *fc_partexpr = NIL;
		List	   *fc_nullable_partexpr = NIL;
		ListCell   *fc_lc;

		switch (fc_jointype)
		{
				/*
				 * 由INNER连接产生的连接关系可以被视为根据任一内外关系键进行分区。
				 * 例如，A INNER JOIN B ON A.a = B.b可以被视为根据A.a或B.b进行分区。
				 * 因此，我们将这两个键都添加到joinrel的分区表达式列表中。
				 * 但是，任何已经是可空的仍然必须被视为可空。
				 */
			case JOIN_INNER:
				fc_partexpr = list_concat_copy(fc_outer_expr, fc_inner_expr);
				fc_nullable_partexpr = list_concat_copy(fc_outer_null_expr,
													 fc_inner_null_expr);
				break;

				/*
				 * 由SEMI或ANTI连接产生的连接关系可以视为根据外部关系键进行分区。
				 * 内部关系的键不再重要；因为它们在连接输出中不可见，
				 * 所以没有任何东西可以与它们连接。
				 */
			case JOIN_SEMI:
			case JOIN_ANTI:
				fc_partexpr = list_copy(fc_outer_expr);
				fc_nullable_partexpr = list_copy(fc_outer_null_expr);
				break;

				/*
				 * 由LEFT OUTER JOIN产生的连接关系同样可以视为根据（不可空）外部关系键进行分区。
				 * 只要涉及严格连接操作符，内部（可空）关系键也可以作为分区键用于进一步连接。
				 */
			case JOIN_LEFT:
				fc_partexpr = list_copy(fc_outer_expr);
				fc_nullable_partexpr = list_concat_copy(fc_inner_expr,
													 fc_outer_null_expr);
				fc_nullable_partexpr = list_concat(fc_nullable_partexpr,
												fc_inner_null_expr);
				break;

				/*
				 * 对于FULL OUTER JOIN，两者关系都是可空的，
				 * 因此，生成的连接关系可以视为根据内外关系的任一键进行分区，
				 * 但仅限于涉及严格连接操作符的连接。
				 */
			case JOIN_FULL:
				fc_nullable_partexpr = list_concat_copy(fc_outer_expr,
													 fc_inner_expr);
				fc_nullable_partexpr = list_concat(fc_nullable_partexpr,
												fc_outer_null_expr);
				fc_nullable_partexpr = list_concat(fc_nullable_partexpr,
												fc_inner_null_expr);

				
/*
				 * 还要添加与每个可能的全连接输出变量对应的 CoalesceExprs
				 * （即左侧合并到右侧），以便我们可以使用这些变量匹配等值连接表达式。
				 * 我们实际上只需要这些用于通过 JOIN USING 合并的列，并且
				 * 仅与对应于解析分析将为这些变量构建的数据结构的输入项对。
				 * 但是很难判断这些是什么，因此只需生成所有对。
				 * nullable_partexprs 列表中的额外项不会导致大问题。
				 * （这些项可能会与用户编写的 COALESCE 进行匹配，但
				 * 在这些上进行分区仍然是有效的，因为它们将是分区列或 NULL；
				 * 这与任何外连接的按分区嵌套的论点是一样的。）
				 * 我们假设没有类型转换需要生成 coalesce 表达式，
				 * 因为不同类型的列不会被分类为相同的 PartitionScheme。
				 */
				foreach(fc_lc, list_concat_copy(fc_outer_expr, fc_outer_null_expr))
				{
					Node	   *fc_larg = (Node *) lfirst(fc_lc);
					ListCell   *fc_lc2;

					foreach(fc_lc2, list_concat_copy(fc_inner_expr, fc_inner_null_expr))
					{
						Node	   *fc_rarg = (Node *) lfirst(fc_lc2);
						CoalesceExpr *fc_c = makeNode(CoalesceExpr);

						fc_c->coalescetype = exprType(fc_larg);
						fc_c->coalescecollid = exprCollation(fc_larg);
						fc_c->args = list_make2(fc_larg, fc_rarg);
						fc_c->location = -1;
						fc_nullable_partexpr = lappend(fc_nullable_partexpr, fc_c);
					}
				}
				break;

			default:
				elog(ERROR, "unrecognized join type: %d", (int) fc_jointype);
		}

		fc_joinrel->partexprs[fc_cnt] = fc_partexpr;
		fc_joinrel->nullable_partexprs[fc_cnt] = fc_nullable_partexpr;
	}
}

/*
 * build_child_join_reltarget
 *	  从父连接关系设置子连接关系的 reltarget。
 */
static void fc_build_child_join_reltarget(PlannerInfo *fc_root,
						   RelOptInfo *fc_parentrel,
						   RelOptInfo *fc_childrel,
						   int fc_nappinfos,
						   AppendRelInfo **fc_appinfos)
{
	/* 构建目标列表 */
	fc_childrel->reltarget->exprs = (List *)
		adjust_appendrel_attrs(fc_root,
							   (Node *) fc_parentrel->reltarget->exprs,
							   fc_nappinfos, fc_appinfos);

	/* 设置成本和宽度字段 */
	fc_childrel->reltarget->cost.startup = fc_parentrel->reltarget->cost.startup;
	fc_childrel->reltarget->cost.per_tuple = fc_parentrel->reltarget->cost.per_tuple;
	fc_childrel->reltarget->width = fc_parentrel->reltarget->width;
}
